/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.flink.runtime.util;

import org.apache.flink.util.ExceptionUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.jar.JarFile;

/**
 * Utilities for information with respect to class loaders, specifically class loaders for the
 * dynamic loading of user defined classes.
 */
public final class ClassLoaderUtil {

    /**
     * Gets information about URL class loaders. The returned info string contains all URLs of the
     * class loader. For file URLs, it contains in addition whether the referenced file exists, is a
     * valid JAR file, or is a directory.
     *
     * <p>NOTE: This method makes a best effort to provide information about the classloader, and
     * never throws an exception.
     *
     * <p>NOTE: Passing {@code ClassLoader.getSystemClassLoader()} on Java 9+ will not return
     * anything interesting.
     *
     * @param loader The classloader to get the info string for.
     * @return The classloader information string.
     */
    public static String getUserCodeClassLoaderInfo(ClassLoader loader) {
        if (loader instanceof URLClassLoader) {
            URLClassLoader cl = (URLClassLoader) loader;

            try {
                StringBuilder bld = new StringBuilder();

                if (cl == ClassLoader.getSystemClassLoader()) {
                    bld.append("System ClassLoader: ");
                } else {
                    bld.append("URL ClassLoader:");
                }

                for (URL url : cl.getURLs()) {
                    bld.append(formatURL(url));
                }

                return bld.toString();
            } catch (Throwable t) {
                return "Cannot access classloader info due to an exception.\n"
                        + ExceptionUtils.stringifyException(t);
            }
        } else if (loader == ClassLoader.getSystemClassLoader()) {
            // this branch is only reachable on Java9+
            // where the system classloader is no longer a URLClassLoader
            return "System ClassLoader";
        } else {
            return "No user code ClassLoader";
        }
    }

    /**
     * Returns the interpretation of URL in string format.
     *
     * <p>If the URL is null, it returns '(null)'.
     *
     * <p>If the URL protocol is file, prepend 'file:' flag before the formatted URL. Otherwise, use
     * 'url: ' as the prefix instead.
     *
     * <p>Also, it checks whether the object that the URL directs to exists or not. If the object
     * exists, some additional checks should be performed in order to determine that the object is a
     * directory or a valid/invalid jar file. If the object does not exist, a missing flag should be
     * appended.
     *
     * @param url URL that should be formatted
     * @return The formatted URL
     * @throws IOException When JarFile cannot be closed
     */
    public static String formatURL(URL url) throws IOException {
        StringBuilder bld = new StringBuilder();
        bld.append("\n    ");
        if (url == null) {
            bld.append("(null)");
        } else if ("file".equals(url.getProtocol())) {
            String filePath = url.getPath();
            File fileFile = new File(filePath);

            bld.append("file: '").append(filePath).append('\'');

            if (fileFile.exists()) {
                if (fileFile.isDirectory()) {
                    bld.append(" (directory)");
                } else {
                    JarFile jar = null;
                    try {
                        jar = new JarFile(filePath);
                        bld.append(" (valid JAR)");
                    } catch (Exception e) {
                        bld.append(" (invalid JAR: ").append(e.getMessage()).append(')');
                    } finally {
                        if (jar != null) {
                            jar.close();
                        }
                    }
                }
            } else {
                bld.append(" (missing)");
            }
        } else {
            bld.append("url: ").append(url);
        }
        return bld.toString();
    }

    /**
     * Checks, whether the class that was not found in the given exception, can be resolved through
     * the given class loader.
     *
     * @param cnfe The ClassNotFoundException that defines the name of the class.
     * @param cl The class loader to use for the class resolution.
     * @return True, if the class can be resolved with the given class loader, false if not.
     */
    public static boolean validateClassLoadable(ClassNotFoundException cnfe, ClassLoader cl) {
        try {
            String className = cnfe.getMessage();
            Class.forName(className, false, cl);
            return true;
        } catch (ClassNotFoundException e) {
            return false;
        } catch (Exception e) {
            return false;
        }
    }

    /** Private constructor to prevent instantiation. */
    private ClassLoaderUtil() {
        throw new RuntimeException();
    }
}
